﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace lib.cc65
{
    public class BuildTool : IDisposable
    {
        System.Diagnostics.Process process;
        Thread readthread;
        Thread readerrthread;
        void CopyFolder(System.IO.DirectoryInfo source, string dest)
        {
            foreach (var f in source.GetFiles())
            {
                if (System.IO.Directory.Exists(dest) == false)
                    System.IO.Directory.CreateDirectory(dest);

                f.CopyTo(System.IO.Path.Combine(dest, f.Name));
            }
            foreach (var d in source.GetDirectories())
            {
                var outpath = System.IO.Path.Combine(dest, d.Name);
                if (System.IO.Directory.Exists(outpath) == false)
                    System.IO.Directory.CreateDirectory(outpath);
                CopyFolder(d, outpath);
            }
        }
        string outFolder;
        neside3.ext.ILogger logger;
        List<string> outfile;
        public class Line
        {
            public string txt;
            public bool bError;
        }
        System.Collections.Concurrent.ConcurrentQueue<Line> lines = new System.Collections.Concurrent.ConcurrentQueue<Line>();
        public bool Start(neside3.ext.ILogger logger, string inputFolder, string outputFolder)
        {
            this.outfile = new List<string>();
            this.logger = logger;
            this.outFolder = outputFolder;
            this.lines = new System.Collections.Concurrent.ConcurrentQueue<Line>();
            try
            {
                if (System.IO.Directory.Exists(outFolder) == true)
                    System.IO.Directory.Delete(outFolder, true);
                System.IO.Directory.CreateDirectory(outFolder);
            }
            catch
            {
                logger.Error("目录无法清理:" + outFolder);
                return false;
            }
            if (process != null)
                throw new Exception("once have one process only.");

            CopyFolder(new System.IO.DirectoryInfo(inputFolder), outputFolder);

            var path = System.IO.Path.GetDirectoryName(this.GetType().Assembly.Location);

            System.Diagnostics.ProcessStartInfo p = new System.Diagnostics.ProcessStartInfo();
            p.EnvironmentVariables["PATH"] = path + "\\build_tool\\cc65\\bin";
            p.EnvironmentVariables["CC65_HOME"] = path + "\\build_tool\\cc65";
            p.FileName = "cmd";
            p.Arguments = "";
            p.RedirectStandardOutput = true;
            p.RedirectStandardError = true;
            p.RedirectStandardInput = true;
            p.UseShellExecute = false;
            p.CreateNoWindow = true;
            process = System.Diagnostics.Process.Start(p);

            readthread = new Thread(() =>
             {
                 while (process != null && process.HasExited == false)
                 {
                     string line = process.StandardOutput.ReadLine();
                     lines.Enqueue(new Line() { txt = line, bError = false });
                 }
             });
            readthread.Start();

            readerrthread = new Thread(() =>
            {
                while (process != null && process.HasExited == false)
                {
                    string line = process.StandardError.ReadLine();
                    lines.Enqueue(new Line() { txt = line, bError = true });
                }
            });
            readerrthread.Start();
            System.Threading.Thread.Sleep(100);
            var rt = ReadLines();
            return true;
        }
        Random ran = new Random();
        public List<Line> ReadLines()
        {
            var endtag = "_endtag" + ran.Next();
            process.StandardInput.WriteLine("echo " + endtag);

            List<Line> result = new List<Line>();
            while (true)
            {
                var b = lines.TryDequeue(out Line line);
                if (b)
                {
                    if (string.IsNullOrWhiteSpace(line.txt) || line.txt.IndexOf("_endtag") > 0) continue;
                    if (line.txt == endtag)
                    {
                        return result;
                    }
                    else
                        result.Add(line);
                }
            }
        }
        void annotate_intermediary(string sfile)
        {
            var source_basename = System.IO.Path.GetFileNameWithoutExtension(sfile);
            var foutfile = sfile.Substring(0, sfile.Length - 2) + ".s2";
            var fmapfile = sfile.Substring(0, sfile.Length - 2) + ".map";
            var fout = new System.IO.StreamWriter(foutfile);
            var fmap = new System.IO.StreamWriter(fmapfile);
            var contentlines = System.IO.File.ReadAllLines(sfile);
            var is_code = 0;// is_code = n = 0
            var n = 0;
            string code_text = "";
            foreach (var line in contentlines)// for line in content.split('\n'):
            {
                if (string.IsNullOrWhiteSpace(line))// if not line:
                    continue;// continue
                //# cc65 outputs source code as three commented lines.
                if (line[0] == ';')// if line[0] == ';':
                {
                    is_code += 1;// is_code += 1
                }
                else// else:
                    is_code = 0;//  is_code = 0
                //                # cc65 disables debuginfo, turn it back on.
                if (Regex.Match(line, "\\W*.debuginfo.*off").Success)//if re.match(r'\W*.debuginfo.*off', line):
                {
                    fout.WriteLine(".debuginfo on");// fout.write('.debuginfo on\n')
                    continue;//continue
                }
                if (Regex.Match(line, "\\W*.debuginfo.*-").Success)//if re.match(r'\W*.debuginfo.*-', line):
                {
                    fout.WriteLine(".debuginfo +");// fout.write('.debuginfo on\n')
                    continue;//continue
                }
                fout.WriteLine(line); //fout.write(line + '\n')
                                      //# The source code appears on the middle commented line.
                if (is_code == 2)//if is_code == 2:
                {
                    code_text = line.Substring(1);//code_text = line[1:]
                }
                else if (is_code == 3)// elif is_code == 3:
                {// # Don't output source code for raw assembly, because it's not needed.
                    if (code_text.Contains("__asm__"))// in code_text:// if '__asm__' in code_text:
                        continue;//continue;
                    if (n > 0)
                    {
                        var sfout = "_Rsource_map__" + source_basename + "__" + n.ToString("d04") + ":";
                        var sfmap = source_basename + "__" + n.ToString("d04") + " " + code_text;
                        fout.WriteLine(sfout);
                        fmap.WriteLine(sfmap);
                        //fout.write('_Rsource_map__%s__%04d:\n' % (source_basename, n))
                        //fmap.write('%s__%04d %s\n' % (source_basename, n, code_text))
                    }
                    n += 1;
                }
            }
            fout.Close();
            fmap.Close();

        }
        public bool Build(params string[] files)
        {
            int count = 0;
            int succ = 0;
            foreach (var file in files)
            {
                var ext = System.IO.Path.GetExtension(file).ToLower();
                if (ext == ".c")
                {
                    var outs = file.Substring(0, file.Length - 2) + ".s";
                    //var outfile = file.Substring(0, file.Length - 2) + ".out";
                    process.StandardInput.WriteLine("cc65 --debug-info -Oris --add-source " + file);
                    var rt = ReadLines();
                    bool bsucc = true;
                    foreach (var l in rt)
                    {
                        if (l.bError)
                        {
                            logger.Error(l.txt);
                            bsucc = false;
                        }
                        else
                        {
                            logger.Log(l.txt);
                        }
                    }
                    if (bsucc)
                    {
                        //生成代码后，先修改.s文件，插入_开头的标签，并切换debug信息，这样，用这个s文件编译为.o
                        //就会包含我们插入的标签
                        //这个标签就是源代码
                        annotate_intermediary(outs);
                        var outs2 = file.Substring(0, file.Length - 2) + ".s2";
                        process.StandardInput.WriteLine("ca65 -g " + outs2);
                        rt = ReadLines();

                        foreach (var l in rt)
                        {
                            if (l.bError)
                            {
                                logger.Error(l.txt);
                                bsucc = false;
                            }
                            else
                            {
                                logger.Log(l.txt);
                            }
                        }
                    }

                    count++;
                    if (bsucc)
                    {
                        succ++;
                    }

                }
                else if (ext == ".s")
                {
                    process.StandardInput.WriteLine("ca65 " + file);
                    var rt = ReadLines();
                    bool bsucc = true;
                    foreach (var l in rt)
                    {
                        if (l.bError)
                        {
                            logger.Error(l.txt);
                            bsucc = false;
                        }
                        else
                        {
                            logger.Log(l.txt);
                        }
                    }
                    count++;
                    if (bsucc)
                        succ++;
                }
                var outo = file.Substring(0, file.Length - 2) + ".o";
                outfile.Add(outo);
            }
            logger.Log("build = " + count + " succ = " + succ);
            return count == succ;
        }
        int read_num_prg_banks(string rom_file)
        {
            using (var f = System.IO.File.OpenRead(rom_file))
            {
                var bytes = new byte[5];
                f.Read(bytes, 0, 5);
                return bytes[4];
            }
            //          """Get the number of prg banks from the ROM header."""
            //try:
            //  fp = open(rom_file, 'r')
            //except IOError:
            //  return None
            //bytes = fp.read(5)
            //fp.close()
            //if len(bytes) < 5:
            //  return 0
            //return ord(bytes[4])
        }
        void parse_map_file(Dictionary<string, string> map, string filename)
        {
            //"""Read .map file, convert it to a dict from meta-labels to source code."""
            var lines = System.IO.File.ReadAllLines(filename);
            foreach (var line in lines)
            {
                if (string.IsNullOrWhiteSpace(line))
                    continue;
                var pos = line.IndexOf(' ');
                var num = line.Substring(0, pos);
                var code_text = line.Substring(pos + 2);
                map[num] = code_text;
            }
            //map = { }
            //          for line in content.split('\n'):
            //  if not line:
            //                  continue
            //  pos = line.find(' ')
            //  num = line[0:pos]
            //  code_text = line[pos + 2:]
            //  map[num] = code_text
            //return map
        }
        List<KeyValuePair<string, string>> parse_ln_file(string filename)
        {
            List<KeyValuePair<string, string>> accum = new List<KeyValuePair<string, string>>();
            var lines = System.IO.File.ReadAllLines(filename);
            foreach (var line in lines)
            {
                if (string.IsNullOrWhiteSpace(line))
                    continue;
                if (line[0] == ';')
                    continue;
                var tts = line.Split(' ');
                var address = tts[1].Substring(2);
                var label = tts[2].Substring(1);
                accum.Add(new KeyValuePair<string, string>(address, label));
            }
            accum.Sort((a, b) => a.Key.CompareTo(b.Key));
            return accum;
            //          """Read .ln file, convert it to a sorted list of address and labels."""
            //content = read_file(filename)
            //accum = []
            //for line in content.split('\n'):
            //  if not line:
            //                  continue
            //  if line[0] == ';':
            //    continue
            //  (oper, address, label) = line.split()
            //  address = address[2:]
            //  label = label[1:]
            //  accum.append([address, label])
            //accum.sort(key = lambda x: x[0])
            //return accum
        }
        class citem
        {
            public string address;
            public string first;
            public string second;
        }
        List<citem> combine_ln_and_map(List<KeyValuePair<string, string>> items, Dictionary<string, string> source_map)
        {
            //"""Merge .ln and .map files, making a list of addresses and source code."""
            Dictionary<string, citem> combined = new Dictionary<string, citem>();
            string code_text = "";
            foreach (var item in items)
            {
                var address = item.Key;
                var label = item.Value;
                if (label.StartsWith("_Rsource_map__"))
                {
                    var key = label.Substring(14);
                    code_text = source_map[key];
                    if (combined.ContainsKey(address) == false) combined[address] = new citem();
                    combined[address].second = code_text;
                }
                else
                {
                    if (combined.ContainsKey(address) == false) combined[address] = new citem();
                    combined[address].first = label;
                }
            }
            var keys = new List<string>(combined.Keys);
            keys.Sort();
            List<citem> accum = new List<citem>();
            foreach (var address in keys)
            {
                var elem = combined[address];
                accum.Add(new citem() { address = address, first = elem.first, second = elem.second });
            }
            return accum;
            //for address, label in items:
            //  if label.startswith('_Rsource_map__'):
            //    key = label[14:]
            //    code_text = source_map[key]
            //    if code_text.startswith('void '):
            //      continue
            //    combined[address].second = code_text
            //  else:
            //    combined[address].first = label
            //accum = []
            //for address in sorted(combined.keys()) :
            //  elem = combined[address]
            //  accum.append([address, elem.first, elem.second])
            //return accum
        }
        void write_build_listing(List<citem> items, string outfile)
        {
            //"""Write contents of a single listing file."""
            var fout = new System.IO.StreamWriter(outfile);
            foreach (var item in items)
            {
                var address = item.address;
                var label = item.first;
                var comment = item.second;
                if (label != null && label.IndexOf("__BSS_") == 0)
                    continue;
                else if (label != null && Regex.Match(label, "L[1-9A-F]{4}").Success)
                    continue;
                else
                {
                    var rom_start = ((int.Parse(address, System.Globalization.NumberStyles.HexNumber) / 0x4000) * 0x4000).ToString("X04");
                    if (label == null) label = "";
                    if (comment == null) comment = "";
                    fout.WriteLine("$" + address + "#" + label + "#" + comment);
                }

            }
            fout.Close();
            //fout = open(outfile, 'w')
            //for address, label, comment in items:
            //  if label and label[0:6] == '__BSS_':
            //    continue
            //  elif label and re.match(r'L[1-9A-F]{4}', label):
            //    continue
            //  else:
            //    rom_start = ('%04x' % ((int(address, 16) / 0x4000) * 0x4000)).upper()
            //    fout.write('$%s#%s#%s\n' % (address, label or '', comment or ''))
            //fout.close()
        }
        void copy_build_listing(string orig_file, int banks)
        {
            for(var i=0;i<banks;i++)
            {
                var replace_suffix = ".nes." + i + ".nl";
                var dest_file = orig_file.Replace(".nes.ram.nl", replace_suffix);
                System.IO.File.Copy(orig_file, dest_file, true);
            }
        }
        void labelprocess(string build_target, string listing_file)
        {
            //# Process command line args.
            //            args = sys.argv[1:]
            //  num_prg = None
            //  try:
            //    pos = args.index('--num-prg')
            //    num_prg = int(args[pos + 1])
            //    args = args[:pos] + args[pos + 2:]
            //  except:
            //            pass
            //          (link_objects, build_target, listing_file) = extract_args(args)
            //  if '-Ln' not in args:
            //            args.append('-Ln')
            //    args.append(listing_file)

            //  # Call the linker.
            //            link_file(args)

            //  # Get number of prg banks.
            int prg_banks = read_num_prg_banks(build_target);
            //            prg_banks = read_num_prg_banks(build_target)
            //  if num_prg is not None:
            //    prg_banks = num_prg
            //  if prg_banks is None:
            //    sys.stderr.write('ERROR: Cannot read number prg from ROM, '
            //                     'use --num-prg flag\n')
            //    sys.exit(1)

            //  # Parse source map for each object.
            Dictionary<string, string> source_map = new Dictionary<string, string>();
            foreach (var o in outfile)
            {
                var obj_dir = System.IO.Path.GetDirectoryName(o);
                var obj_name = System.IO.Path.GetFileName(o);
                var obj_base = System.IO.Path.GetFileNameWithoutExtension(o);
                var map_file = System.IO.Path.Combine(obj_dir, obj_base + ".map");
                if (System.IO.File.Exists(map_file))
                    parse_map_file(source_map, map_file);
            }
            //            for obj in link_objects:

            //              obj_dir = os.path.dirname(obj)

            //              obj_name = os.path.basename(obj)

            //              obj_base, obj_ext = os.path.splitext(obj_name)

            //              map_file = os.path.join(obj_dir, '.annotate.' + obj_base + '.map')
            //              if os.path.isfile(map_file):
            //      source_map.update(parse_map_file(map_file))

            //  # Read listing and merge with source map.
            var ln_data = parse_ln_file(listing_file);
            var items = combine_ln_and_map(ln_data, source_map);

            //  # Create listings.
            var build_dir = System.IO.Path.GetDirectoryName(build_target);
            var build_base = System.IO.Path.GetFileNameWithoutExtension(build_target);
            var build_listing = System.IO.Path.Combine(build_dir, build_base + ".nes.ram.nl");
            write_build_listing(items, build_listing);
            copy_build_listing(build_listing, prg_banks);
        }
        public bool Link(string resultname, string config, params string[] extlibs)
        {
            string libs = " ";
            foreach (var o in outfile)
            {
                var file = System.IO.Path.Combine(this.outFolder, o);
                if (!System.IO.File.Exists(file))
                {
                    file = o;
                }
                libs += file + " ";
            }
            foreach (var o in extlibs)
            {
                var file = System.IO.Path.Combine(this.outFolder, o);
                if (!System.IO.File.Exists(file))
                {
                    file = o;
                }
                libs += file + " ";
            }
            var configfile = System.IO.Path.Combine(this.outFolder, config);
            if (!System.IO.File.Exists(configfile))
            {
                configfile = config;
            }

            var labelfile = System.IO.Path.Combine(this.outFolder, "labels.txt");
            var resultfile = System.IO.Path.Combine(this.outFolder, resultname);
            var dbgfile = System.IO.Path.Combine(this.outFolder, "debug.dbg");


            var line = "ld65 -Ln " + labelfile + " --dbgfile " + dbgfile + " -C " + configfile + " -o " + resultfile + libs;
            process.StandardInput.WriteLine(line);
            var rt = ReadLines();
            bool bsucc = true;
            foreach (var l in rt)
            {
                if (l.bError)
                {
                    logger.Error(l.txt);
                    bsucc = false;
                }
                else
                {
                    logger.Log(l.txt);
                }
            }
            logger.Log("link = " + bsucc);
            if (bsucc)
            {
                labelprocess(resultfile, labelfile);
                return true;
            }
            //ld65 - C nrom_32k_vert.cfg - o % name %.nes crt0.o % name %.o nes.lib - Ln labels.txt

            return false;
        }
        public void End()
        {
            if (process != null)
            {
                process.Kill();
                process = null;
            }
            if (readthread != null)
            {
                try
                {
                    readthread.Abort();
                }
                catch
                {

                }
                readthread = null;

            }
            if (readerrthread != null)
            {
                try
                {
                    readerrthread.Abort();
                }
                catch
                {

                }
                readerrthread = null;
            }
        }

        public void Dispose()
        {
            this.End();
        }
    }
}
